﻿namespace Home
{
    using System;
    using System.Collections.Generic;
    using System.Configuration;
    using System.Data.Entity;
    using System.Globalization;
    using System.Linq;
    using System.Security.Cryptography.X509Certificates;
    using System.Text;
    using System.Threading;
    using System.Threading.Tasks;
    using Home.ErrorHandling;
    using Home.Repository;
    using Home.Security;
    using Home.Services;
    using Newtonsoft.Json.Linq;

    public abstract class WebsiteCommand : Command
    {
        public override async Task ExecuteAsync(IEnumerable<string> arguments)
        {
            var start = GetSynchronizationStartTime(arguments);

            var time = start;
            var status = default(WebsiteSynchronizationStatus);
            do
            {
                status = await GetWebsiteSynchronizationStatusAsync(this.SynchronizationType, time);
                switch (status)
                {
                    case WebsiteSynchronizationStatus.Success:
                        //// do nothing
                        break;
                    case WebsiteSynchronizationStatus.None:
                    case WebsiteSynchronizationStatus.Failure:
                    case WebsiteSynchronizationStatus.InProgress:
                        //// we do NOT support multiple instances, so treat in progress status as failure
                        await ExecuteAsyncCore(time);
                        break;
                    default:
                        throw new InvalidOperationException(string.Format("The synchronization status {0} is invalid.", status));
                }

                time = time.AddDays(-1);
            } while (status != WebsiteSynchronizationStatus.Success);

            var json = new JObject();
            json["category"] = this.OptimizationCategory;
            json["time"] = new DateTimeOffset(start.Year, start.Month, start.Day, 23, 59, 59, 999, start.Offset);
            var url = $"{WebServiceRootUrl}Remove()";
            await ODataClientHelper.InvokePostAsync(url,
                json,
                client =>
                {
                    client.DefaultRequestHeaders.Add("home-request-id", SecurityHelper.NewRequestId(EncryptionCertificate));
                });
        }

        protected abstract WebsiteSynchronizationType SynchronizationType { get; }

        protected abstract string EntitySetName { get; }

        protected abstract bool TryGetEntity(ServiceDbContext database, Guid websiteId, out WebsiteEntity entity);

        protected abstract void FulfillEntity(JToken json, WebsiteEntity entity);

        protected abstract void AddEntity(ServiceDbContext database, WebsiteEntity entity);

        protected abstract string OptimizationCategory { get; }

        private static X509Certificate2 EncryptionCertificate
        {
            get { return SecurityHelper.FindCertificate(StoreLocation.LocalMachine, StoreName.My, X509FindType.FindByThumbprint, ConfigurationManager.AppSettings["home:EncryptionCertificateThumbprint"]); }
        }

        private static string WebServiceRootUrl
        {
            get { return ConfigurationManager.AppSettings["home:OptimizationServiceRootUrl"]; }
        }

        private async Task ExecuteAsyncCore(DateTimeOffset time)
        {
            await RecordWebsiteSynchronizationStatusAsync(this.SynchronizationType, time, WebsiteSynchronizationStatus.InProgress);

            var status = default(WebsiteSynchronizationStatus);
            try
            {
                var beginTime = ODataHelper.ToODataFormat(time);
                var endTime = ODataHelper.ToODataFormat(new DateTimeOffset(time.Year, time.Month, time.Day, 23, 59, 59, 999, time.Offset));
                var url = $"{WebServiceRootUrl}{this.EntitySetName}?$filter=Created ge {beginTime} and Created le {endTime}";
                await ODataClientHelper.GetServerPagingStepAsync(url, async array =>
                {
                    var database = default(ServiceDbContext);

                    try
                    {
                        var now = DateTimeOffset.UtcNow;
                        var user = Thread.CurrentPrincipal.Identity.Name;
                        database = new ServiceDbContext();

                        foreach (var json in array)
                        {
                            var websiteId = (Guid)json["Id"];
                            var entity = default(WebsiteEntity);
                            var exists = TryGetEntity(database, websiteId, out entity);
                            if (!exists)
                            {
                                entity.Id = Guid.NewGuid();
                                entity.Created = now;
                                entity.CreatedBy = user;
                                entity.WebsiteId = websiteId;
                            }

                            entity.Modified = now;
                            entity.ModifiedBy = user;
                            entity.WebsiteCreated = (DateTimeOffset)json["Created"];
                            entity.WebsiteCreatedBy = (string)json["CreatedBy"];
                            entity.WebsiteModified = (DateTimeOffset)json["Modified"];
                            entity.WebsiteModifiedBy = (string)json["ModifiedBy"];

                            FulfillEntity(json, entity);

                            if (!exists)
                            {
                                AddEntity(database, entity);
                            }
                        }

                        await database.SaveChangesAsync();
                    }
                    finally
                    {
                        if (database != null) database.Dispose();
                    }
                },
                client =>
                {
                    client.DefaultRequestHeaders.Add("home-request-id", SecurityHelper.NewRequestId(EncryptionCertificate));
                });

                status = WebsiteSynchronizationStatus.Success;
            }
            catch (Exception ex)
            {
                status = WebsiteSynchronizationStatus.Failure;
                HandleErrorHelper.Handle(ex);
            }

            await RecordWebsiteSynchronizationStatusAsync(this.SynchronizationType, time, status);
        }

        private static DateTimeOffset GetSynchronizationStartTime(IEnumerable<string> arguments)
        {
            var now = DateTimeOffset.UtcNow;
            var result = new DateTimeOffset(now.Year, now.Month, now.Day, 0, 0, 0, 0, TimeSpan.Zero).AddDays(-1);

            if (arguments != null && arguments.Count() > 0)
            {
                var argument = arguments.ElementAt(0);
                if (!DateTimeOffset.TryParse(argument, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out result))
                {
                    throw new InvalidOperationException(string.Format("The argument {0} is invalid.", argument));
                }
            }

            return result;
        }

        private static async Task<WebsiteSynchronizationStatus> GetWebsiteSynchronizationStatusAsync(WebsiteSynchronizationType type, DateTimeOffset time)
        {
            using (var db = new ServiceDbContext())
            {
                var record = await db.Synchronizations.FirstOrDefaultAsync(sync => sync.Type == type && sync.Time == time);
                return record == null ? WebsiteSynchronizationStatus.None : record.Status;
            }
        }

        private static async Task RecordWebsiteSynchronizationStatusAsync(WebsiteSynchronizationType type, DateTimeOffset time, WebsiteSynchronizationStatus status)
        {
            var now = DateTimeOffset.UtcNow;
            var user = Thread.CurrentPrincipal.Identity.Name;
            using (var db = new ServiceDbContext())
            {
                var record = await db.Synchronizations.FirstOrDefaultAsync(sync => sync.Type == type && sync.Time == time);
                if (record == null)
                {
                    record = new WebsiteSynchronization()
                    {
                        Id = Guid.NewGuid(),
                        Created = now,
                        CreatedBy = user,
                        Modified = now,
                        ModifiedBy = user,
                        Type = type,
                        Time = time,
                        Status = status
                    };
                    db.Synchronizations.Add(record);
                }
                else
                {
                    record.Modified = now;
                    record.ModifiedBy = user;
                    record.Status = status;
                }

                await db.SaveChangesAsync();
            }
        }
    }
}
